Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

22장. Inline과 Reified

7부의 마지막 장입니다.

14장에서 우리는 함수를 값처럼 다루는 법을 배웠습니다.
함수를 인자로 넘기는 고차 함수도 익혔습니다.

그런데 함수를 인자로 넘기면
눈에 보이지 않는 작은 비용이 생깁니다.

이 장에서는 그 비용을 줄이는 inline과,
제네릭의 한계를 넘는 reified를 배웁니다.

이 장도 심화 내용입니다.
inlinereified
“왜 필요한지“만 이해해도 충분합니다.


22.1 고차 함수와 함수 객체

고차 함수를 하나 만들어 봅시다.

fun repeatAction(times: Int, action: () -> Unit) {
    for (i in 1..times) {
        action()
    }
}

repeatAction(3) {
    println("안녕")
}

편리해 보입니다.
그런데 내부에서는 무슨 일이 일어날까요?

{ println("안녕") }이라는 람다는
사실 하나의 “객체“로 만들어집니다.

함수를 넘길 때마다
이런 함수 객체(function object)가 생성됩니다.

한두 번이면 문제없지만,
아주 많이 호출되는 코드에서는
이 작은 비용이 쌓일 수 있습니다.


22.2 inline

이 비용을 없애는 방법이 inline입니다.

inline을 붙이면
컴파일러가 함수 호출을 없애고,
그 자리에 함수 내용을 직접 붙여 넣습니다.

inline fun repeatAction(times: Int, action: () -> Unit) {
    for (i in 1..times) {
        action()
    }
}

비유하자면 이렇습니다.

함수를 “호출“하는 대신,
코드를 그 자리에 “복사해 붙여넣기” 합니다.

그래서 위 호출은 컴파일 후
대략 이런 코드로 바뀝니다.

for (i in 1..3) {
    println("안녕")   // 람다 객체 없이 직접 실행
}

함수 객체가 만들어지지 않으므로
람다를 넘기는 비용이 사라집니다.

단, 무조건 좋은 것은 아닙니다.
코드를 복사해 붙이므로
함수가 크면 결과물 크기가 커집니다.

inline은 람다를 받는
작은 고차 함수에 쓰는 것이 적절합니다.


22.3 noinline

inline 함수가 람다를 여러 개 받을 때,
그중 일부만 인라인하고 싶을 수 있습니다.

인라인하고 싶지 않은 람다에는
noinline을 붙입니다.

inline fun process(
    action1: () -> Unit,
    noinline action2: () -> Unit
) {
    action1()          // 인라인됨
    val saved = action2 // noinline이라 객체로 다룰 수 있음
    saved()
}

인라인된 람다는 객체가 아니라서
변수에 저장하거나 다른 곳에 넘길 수 없습니다.
그런 조작이 필요하면 noinline을 씁니다.


22.4 crossinline

인라인된 람다는 특별한 능력이 있습니다.
바깥 함수를 통째로 종료하는
return을 쓸 수 있다는 점입니다.

이를 비지역 반환(non-local return)이라고 합니다.

inline fun run(action: () -> Unit) {
    action()
}

fun test() {
    run {
        return   // test() 자체를 종료함
    }
    println("여기는 실행 안 됨")
}

그런데 람다를 다른 곳에서 실행하는 경우
이 비지역 반환이 위험해질 수 있습니다.

이를 막으려면 crossinline을 붙입니다.

inline fun runLater(crossinline action: () -> Unit) {
    val runnable = Runnable {
        action()   // 여기서 return 금지
    }
    runnable.run()
}

crossinline
“인라인은 유지하되, 비지역 반환은 금지한다“는 뜻입니다.


22.5 JVM의 Type Erasure

이제 reified로 넘어가기 위한
배경 지식을 하나 봅시다.

JVM에는 타입 소거(Type Erasure)라는 특징이 있습니다.

제네릭의 타입 정보가
실행 시점(runtime)에는 지워진다는 뜻입니다.

즉, List<String>List<Int>
컴파일 후에는 둘 다 그냥 List가 됩니다.

그래서 이런 코드는 아예 작성할 수 없습니다.

fun <T> isType(value: Any): Boolean {
    return value is T   // 오류! 실행 시점에 T를 알 수 없음
}

T가 무엇이었는지
실행할 때는 정보가 사라졌기 때문입니다.


22.6 reified

reified는 이 한계를 넘게 해 줍니다.

inline 함수 안에서는
코드를 그 자리에 복사해 붙이므로,
T 자리에 실제 타입이 그대로 들어갑니다.

그래서 타입 정보가 사라지지 않습니다.

이때 타입 파라미터에 reified를 붙이면
실행 시점에도 그 타입을 쓸 수 있습니다.

inline fun <reified T> isType(value: Any): Boolean {
    return value is T   // OK! reified 덕분에 가능
}

println(isType<String>("hello"))   // true
println(isType<Int>("hello"))      // false

reified는 반드시 inline과 함께 써야 합니다.
인라인이 되어야 타입이 실제로 채워지기 때문입니다.


22.7 런타임 타입을 사용하는 Generic 함수

reified가 실무에서 빛나는 순간은
“타입을 기준으로 무언가를 만들거나 변환할 때“입니다.

예를 들어 JSON 문자열을
원하는 타입의 객체로 바꾸는 함수를 상상해 봅시다.

inline fun <reified T> parse(json: String): T {
    // 실제로는 라이브러리가 T 타입 정보를 활용해 변환
    return jsonLibrary.readValue(json, T::class.java)
}

val user: User = parse<User>(jsonText)

reified 덕분에
함수 안에서 T::class처럼
타입 자체를 직접 다룰 수 있습니다.

스프링이나 각종 라이브러리에서
이런 패턴을 자주 만나게 됩니다.
그래서 지금 원리를 알아 두면
나중에 코드가 훨씬 잘 읽힙니다.


22장을 마치며

이 장에서 우리는 다음을 배웠습니다.

  • 고차 함수는 람다를 함수 객체로 만들어 작은 비용이 생긴다는 점
  • inline은 코드를 그 자리에 붙여 넣어 그 비용을 없앤다는 점
  • noinlinecrossinline으로 인라인 동작을 세밀하게 조절한다는 점
  • JVM은 타입 소거 때문에 실행 시점에 제네릭 타입을 모른다는 점
  • reified(+inline)로 실행 시점에도 타입을 쓸 수 있다는 점

이것으로 7부(타입 시스템 심화)를 마칩니다.
다음 8부에서는 예외와 오류를 설계합니다.